<?php

class Status_Object_Status {

  //these data are stored in the structured data
  protected $_id;
  protected $_private;
  
  //other data are stored in a blob and need to be extracted
  protected $_userId;
  protected $_userEmail;
  protected $_userPseudo;
  protected $_status;
  protected $_publishedTime;
  protected $_tags = array();  
  protected $_mentions = array(); 
    
  /* definition of the status table */
  static protected $_dbStatusTableName = 'statuses';
  static protected $_dbStatusIdCol = 'id';
  static protected $_dbStatusDataCol = 'data';
  static protected $_dbStatusPrivateCol = 'private';
    
  //following data are stored in the data blob and need to be extracted
	static protected $_dbStatusUserGroup = 'user';
	static protected $_dbStatusUserEmailElem = 'email';
	static protected $_dbStatusUserIdElem = 'id';
	static protected $_dbStatusUserPseudoElem = 'pseudo';
  
	static protected $_dbStatusTagsGroup = 'tags';  

	static protected $_dbStatusMentionsGroup = 'mentions';

	static protected $_dbStatusStatusElem = 'status';  
  
  static protected $_dbStatusDateElem = 'date'; 

  /* need to put a limit to the number of pages that can be retrieved */
  static protected $_limitPage = 1000;
  
  /* identifier for the lucene index for statuses */
  static protected $_luceneIndexName = 'statuses';
  
    
  public function __construct($id,$data = array()) {
  	
  	//if the user provides only the id, we need to go to the db to retrieve the data
  	if(empty($data)) {
  	  //We validate that the id is numeric
  	  $validator = new Zend_Validate_Digits();
	  if(!$validator->isValid($id)) {
  	    require_once('status/objects/Exception.php');
  	    throw new Status_Object_Exception('Invalid identifier',8); 	  	
	  }

  	  //We get the data from the database
  	  //statuses are not sharded, best would be a distributed storage for the status
  	  //what will be sharded are the following, list and other features where just the id of the status is needed
  	  $db = self::_getDb();
      $select = $db->select()->from(self::$_dbStatusTableName);
      $select->where($db->quoteInto(self::$_dbStatusIdCol ." = ?", $id));
      $res = $db->fetchAll($select->__toString());
    
      if(empty($res)) {
  	    require_once('status/objects/Exception.php');
  	    throw new Status_Object_Exception('Status not found',8); 
      }
    $data = $res[0];
  	}
  	
    $this->_id = $data[self::$_dbStatusIdCol];
    $this->_private = $data[self::$_dbStatusPrivateCol];

	$decodedBlob = self::decodeStatus($data[self::$_dbStatusDataCol]);
	
	$this->_status = $decodedBlob[self::$_dbStatusStatusElem];
	$this->_publishedTime = $decodedBlob[self::$_dbStatusDateElem];
    
    //get user details
	$this->_userId = $decodedBlob[self::$_dbStatusUserGroup][self::$_dbStatusUserIdElem]; 
	$this->_userEmail = $decodedBlob[self::$_dbStatusUserGroup][self::$_dbStatusUserEmailElem];
	$this->_userPseudo = $decodedBlob[self::$_dbStatusUserGroup][self::$_dbStatusUserPseudoElem];
	
	//get tags for the status
	if(isset($decodedBlob[self::$_dbStatusTagsGroup])) $this->_tags = $decodedBlob[self::$_dbStatusTagsGroup];

	//get mentions for the status
	if(isset($decodedBlob[self::$_dbStatusMentionsGroup])) $this->_mentions = $decodedBlob[self::$_dbStatusMentionsGroup];
		 	
  }	//end constructor


  //The usual getter
  public function getId() {return $this->_id;}
  public function getStatus() {return $this->_status;}
  public function getUserId() {return $this->_userId;} 
  public function getuserEmail() {return $this->_userEmail;}  
  public function getuserPseudo() {return $this->_userPseudo;}  
  public function getpublishedTime() {return $this->_publishedTime;}   

  //tags are stored in lowercase
  public function getTags() {
    return $this->_tags;	
  }  

  //tags are stored in lowercase
  public function getMentions() {
    return $this->_mentions;	
  }  

  public function isPrivate() {
    return (bool) $this->_private;	
  } 
  
  
  //Object model when returned in the API
  public function asArray() {
  	$results = array();

    $results['id'] = $this->getId();
    $results['status'] = $this->getStatus();
    $results['publishedTime'] = $this->getpublishedTime();
    $results['private'] = $this->isPrivate();	
    $results['user']['id'] = $this->getUserId(); 
    $results['user']['email'] = $this->getuserEmail(); 
    $results['user']['pseudo'] = $this->getuserPseudo();
    $results['tags'] = $this->getTags();  	
    $results['mentions'] = $this->getMentions(); 
    return $results;
  }

  //delete - delete the status from the status database (only the status, not the timeline)
  //@input void
  //@return int number of rows deleted (should be 1) if not 1 you better do something
  public function delete() {
  	//First we validate that the authenticated user is the owner of the status
	$userId = (int) Zend_Registry::get('app_user')->getId();
	if($this->getUserId() != $userId) {
  		require_once('status/objects/Exception.php');
  		throw new Status_Object_Exception('user can\'t delete status',15);		
	}
	//We go to the database and delete the status
	$db = self::_getDb();
	return $db->delete(self::$_dbStatusTableName,$db->quoteInto(self::$_dbStatusIdCol.' = ?',$this->getId()));
	
  }

  static public function formatPage($page = 1) {
      if(empty($page)) $page = 1;
      $validator = new Zend_Validate_Digits();
      if(!$validator->isValid($page)) {
  		require_once('status/objects/Exception.php');
  		throw new Status_Object_Exception('invalid input parameters',15);      	
  	  }
  	  if(self::$_limitPage < $page) {
  		require_once('status/objects/Exception.php');
  		throw new Status_Object_Exception('page search is limited to '.self::$_limitPage.' pages',15);   	  	
  	  }
  	  
  	  if($page < 1 ) $page = 1;
  	  return $page;		
  }

  static public function formatPerPage($perPage = 20) {
      if(empty($perPage)) $perPage = 20;
      $validator = new Zend_Validate_Digits();
      if(!$validator->isValid($perPage)) {
  		require_once('status/objects/Exception.php');
  		throw new Status_Object_Exception('invalid input parameters',15);      	
  	  }
  	  if($perPage < 1 ) $perPage = 1;
  	  if($perPage > 100 ) $perPage = 100;
  	  return $perPage;
  }



  
  /* listing() - used to retrieve the latest public statuses
  	@param perPage - number of statuses to return
  	@param page - num of the page to return
  	@return array of php statuses	 
  */
	
  static public function listing($perPage = 20,$page = 1,$beforeStatus = '',$afterStatus = '',$searchMode = '') {
      
      //We validate the inputs
 	  $page = self::formatPage($page);
 	  $perPage = self::formatPerPage($perPage); 	  
  	  
  	  $db = self::_getDb();
      $select = $db->select();
      $select->from(self::$_dbStatusTableName);
      $select->where($db->quoteInto(self::$_dbStatusPrivateCol ." = ?", false));
      $invert = false;
      
      if('page' == $searchMode) {
      	$select->limitPage($page, $perPage);	
      	$select->order(self::$_dbStatusIdCol.' DESC');
      }
      if('beforeStatusId' == $searchMode) {
      	$select->where($db->quoteInto(self::$_dbStatusIdCol ." < ?", $beforeStatus));
        $select->limit($perPage);      	
      	$select->order(self::$_dbStatusIdCol.' DESC');
      }
      if('afterStatusId' == $searchMode) {
      	$select->where($db->quoteInto(self::$_dbStatusIdCol ." > ?", $afterStatus));
   		$select->limit($perPage);      	
      	$select->order(self::$_dbStatusIdCol.' ASC');
      	$invert = true;
      } 

Zend_Registry::get('dt_log')->log('db message for status list:'.$select->__toString(), Zend_Log::DEBUG);
	  
      $res = $db->fetchAll($select->__toString());    	  

      if(empty($res)) return array();

      //We must reinterpret the rows so it is independant to the way it is stored
      $results = array();
      foreach($res as $num => $row) {
    	$status = new Status_Object_Status('',$row);
    	$results[] = $status->asArray();	
      }
      if($invert) krsort($results);
      return $results;
      
  } //end listing
  
  /* is Visible() - tells if the user is allowed to see it or not.
     use cases are if the status is public or the user is owner of the status (could think of other use cases later)
     */
  
  public function isVisible() {
  	//first use case the status is public
  	if(!$this->isPrivate()) return true;
  	
  	//second use case the user is the owner of the status
	$email = Zend_Registry::get('app_user')->getEmail();
  	if($this->getuserEmail() == $email) return true;
  	
  	
  	return false;
  }
  
  
  /* create() - as said in the name, create is responsible for creating a status in the system. 
     only user index is done, other indexing like follower and following ones are left to the controller (asynchronously is better)
     @param text - statusText
     @throw Status_Object_Exception if the status is empty after purification
     @throw Zend_Db_Exception if an issue occurs while putting the message in database
     @return Numeric identifier of the tweet
  */
  static public function create($statusText) {

Zend_Registry::get('dt_log')->log('Status before purification: '.$statusText, Zend_Log::DEBUG);  	

  	//we check that the status is sane
  	//We clean the description, only text is allowed
	require_once('htmlpurifier/library/HTMLPurifier.auto.php');
	$config = HTMLPurifier_Config::createDefault();
	$config->set('HTML.Allowed', '');
  	$purifier = new HTMLPurifier($config);
    $status = $purifier->purify(trim($statusText));

Zend_Registry::get('dt_log')->log('Status after purification: '.$status, Zend_Log::DEBUG);  


  	//the purifier may remove everything
  	if(empty($status)) {
      require_once('status/objects/Exception.php');
      throw new Status_Object_Exception('purified text is empty',15);  		
  	}
  	  	
  	//If the status is too long we throw an exception
  	if(300 < mb_strlen($status)) {
      require_once('status/objects/Exception.php');
      throw new Status_Object_Exception('Status is too long, limit is 300 characters',16);   		
  	}
  	
	//if the status starts with 'd ', then it is a direct message and is not public
	$private = false;
	if(1 == preg_match('/^d /',$status)) {
		$status = preg_replace('/^d /','',$status);
		$private = true;
	}
	
	
  	//we need to get the email of the user
	$user = Zend_Registry::get('app_user');
	$email = $user->getEmail();
	$userId = (int) $user->getId();
	$pseudo = $user->getPseudo();	
	
  	//the date is calculated based on time that gives the current time
  	$publishedTime = time();
 
 	//We check for tags
 	$tags = array();
 	$tagsMatched = preg_match_all(  '/#([a-zA-z0-9_]+)/i'  ,  $status ,  $matches);
	
Zend_Registry::get('dt_log')->log('Number of tags found on status:'.serialize($matches), Zend_Log::DEBUG);
	
 	if($tagsMatched) {
 		foreach($matches[1] as $num => $tag ) {
 			//we check that the tags is not already in the list
			$tagLower = strtolower($tag);
			if(!empty($tags)) {
				foreach($tags as $num => $key) {
					if($key == $tagLower) continue(2);
				}
			}
			//we store the tags in lowercase
 			$tags[] = $tagLower;
 		}	
 	}
 	
 	//we check for @ signs and validate that the user does exist
 	$mentions = array();
 	$mentionsMatched = preg_match_all(  '/@([a-zA-z0-9\._\-]+)/i'  ,  $status ,  $matches);
 	if($mentionsMatched) {
 		require_once('Myzen/User.php');
		
 		foreach($matches[1] as $num => $mention ) {
 			//we check that the user does exists
 			try{
				$mentionUser = new Myzen_User(array('pseudo' => $mention));
			} catch(Zend_Auth_Exception $e) {
				//no match, we go on on the loop
				continue;    		
			} //end catch
			//only keep the pseudo and the id
			$mentionData = array('id'=> $mentionUser->getId(), 'pseudo' => $mentionUser->getPseudo());
			$mentions[] = $mentionData;
			
 		}//end foreach	
 	} //end mentions matched
	 	
  	//we encode the blob and stores it
  	$data = array(
  		self::$_dbStatusStatusElem => $status,
  		self::$_dbStatusDateElem => $publishedTime,
		self::$_dbStatusUserGroup => array(
			self::$_dbStatusUserIdElem => $userId,
			self::$_dbStatusUserEmailElem => $email,
			self::$_dbStatusUserPseudoElem => $pseudo,
		),
		self::$_dbStatusTagsGroup => $tags,
		self::$_dbStatusMentionsGroup => $mentions,			
  	);
	
Zend_Registry::get('dt_log')->log(serialize($data), Zend_Log::DEBUG);
  
  	//we encode the blob
  	$blobData = self::encodeStatus($data);
  	
  	//we store it in the data column
  	$toBesaved = array(self::$_dbStatusDataCol => $blobData,self::$_dbStatusPrivateCol => $private);
  	
  	//and we put it in the database
  	$db = self::_getDb();
  	
    $db->insert(self::$_dbStatusTableName, $toBesaved);
    
    //we get the last inserted Id
    $statusId = $db->lastInsertId();
    
    //we return the id of the status (which controller can use to regenerate the status)
  	return $statusId;

  } //end create


  //the message is stored as a base64 gzdefalted serialized PHP
  static public function decodeStatus($data) {
	
    $decoded = @unserialize(gzinflate(base64_decode($data)));
    if(empty($decoded)) {
      require_once('status/objects/Exception.php');
      throw new Status_Object_Exception('An issue occured while decoding message',16);
    }  
    return  $decoded;	
  }

  static public function encodeStatus($data) {
  	if(!is_array($data)) {
  		require_once('status/objects/Exception.php');
  		throw new Status_Object_Exception('input parameter is not an array',12); 
  	}
    $encoded = @base64_encode(gzdeflate(serialize($data),9));
    if(empty($encoded)) {
    	require_once('status/objects/Exception.php');
    	throw new Status_Object_Exception('An issue occured while encoding message',17);
    } 
    return $encoded;   	
  }
  
  static protected function _getDb() {
  	return Zend_Registry::get('dt_dbs')->getDb('status','statuses','key');	
  }



  //publish the status to the lucene statuses index
  
  public function publishToLucene() {
      
      if($this->isPrivate()) return;
      // Open existing index
      
      Zend_Registry::get('dt_log')->log('publishing status to lucene index',7);
      
      $index = Myzen_Search_Lucene::open(self::$_luceneIndexName);     
      $doc = new Zend_Search_Lucene_Document();
      // Store document URL to identify it in search result.
      $doc->addField(Zend_Search_Lucene_Field::Keyword('statusId',$this->getId()));

      //for the status and the tags we switch to the english analyzer
      Myzen_Search_lucene::switchAnalyzerToEnglishText();
      
      $doc->addField(Zend_Search_Lucene_Field::Keyword('author', $this->getuserPseudo()));
	  $doc->addField(Zend_Search_Lucene_Field::UnStored('time', $this->getpublishedTime()));

	  //tags	  
	  $mentions = $this->getMentions();
	  if(!empty($mentions)) {
	  	$stringMentions = '';
	  	foreach($mentions as $num => $data) $stringMentions .= $data['pseudo'].' '; 
	  	$doc->addField(Zend_Search_Lucene_Field::Unstored('mention', $stringMentions));		
	  }

      

      //We store the status and we also reput the name of the author in case the user makes a general search
	  $doc->addField(Zend_Search_Lucene_Field::UnStored('status', $this->getuserPseudo().' '.$this->getStatus()));

	  //tags	  
	  $tags = $this->getTags();
	  if(!empty($tags)) {
	  	$stringTags = implode(' ', $tags);
	  	$doc->addField(Zend_Search_Lucene_Field::UnStored('tag', $stringTags));		
	  }
	  

	  //mentions
	  


      // Add document to the index.
      $index->addDocument($doc);	
  }


	static public function search($query) {
		$query = trim($query);
		if(empty($query)) return array();

		$index = Myzen_Search_Lucene::open(self::$_luceneIndexName);
		Myzen_Search_Lucene::setDefaultSearchField('status');
      	//for the status and the tags we switch to the english analyzer
      	Myzen_Search_lucene::switchAnalyzerToEnglishText();
      
		$hits = $index->find($query);
		
		if(empty($hits)) return array();
		
		
		
		$statusesData = array();
		$i = 0;
		foreach ($hits as $hit) {
			//limit the dataset returned
			if($i > 20) break;
			try{
				$temp =	new Status_Object_Status($hit->statusId);
				$statusesData[] = $temp->asArray();
				$i++;
			} catch(Exception $e) {}
		
		}
		
		return $statusesData;

	} //end search

}